﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="InvokeMethodExpressionParser.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon.Windows.Input
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;

    internal class InvokeMethodExpressionParser
    {
        public InvokeMethodExpression Parse(string expression)
        {
            if (this.Enumerator != null)
            {
                throw new InvalidOperationException();
            }

            Enumerator = InvokeMethodExpressionTokenizer.Tokenize(expression).GetEnumerator();
            try
            {
                return ParseCore();
            }
            finally
            {
                Enumerator = null;
            }
        }

        private InvokeMethodExpression ParseCore()
        {
            if (!MoveNext())
            {
                return null;
            }

            var methodName = ReadType(InvokeMethodExpressionTokenType.Identifier);
            IEnumerable<InvokeMethodTypeReference> parameterTypes = null;
            if (!IsEof)
            {
                ReadType(InvokeMethodExpressionTokenType.OpenBracket);
                parameterTypes = this.ReadParameterTypes(InvokeMethodExpressionTokenType.CloseBracket);
                ReadType(InvokeMethodExpressionTokenType.CloseBracket);
                if (!IsEof)
                {
                    RaiseUnexpectedToken(Current);
                }
            }

            return new InvokeMethodExpression(methodName, parameterTypes);
        }

        protected IEnumerator<InvokeMethodExpressionToken> Enumerator { get; private set; }

        private bool IsEof
        {
            get { return Enumerator == null; }
        }

        private static void RaiseUnexpectedToken(InvokeMethodExpressionToken token)
        {
            throw new FormatException(string.Format("Unexpected token \"{0}\".", token.Text));
        }

        private InvokeMethodExpressionTokenType CurrentType
        {
            get { return Current.Type; }
        }

        private InvokeMethodExpressionToken Current
        {
            get
            {
                if (IsEof)
                {
                    throw new FormatException("Unexpected end of expression.");
                }

                return this.Enumerator.Current;
            }
        }

        private bool MoveNext()
        {
            if (IsEof)
            {
                return false;
            }

            if (!Enumerator.MoveNext())
            {
                Enumerator = null;
                return false;
            }

            return true;
        }

        private IList<InvokeMethodTypeReference> ReadParameterTypes(params InvokeMethodExpressionTokenType[] terminatingTokenTypes)
        {
            var parameters = new List<InvokeMethodTypeReference>();

            InvokeMethodTypeReference typeReference = null;
            InvokeMethodExpressionToken startToken = null;
            while (!terminatingTokenTypes.Any(x => x == CurrentType))
            {
                startToken = Current;
                switch (startToken.Type)
                {
                    case InvokeMethodExpressionTokenType.Comma:
                        parameters.Add(typeReference ?? new InvokeMethodTypeReference(string.Empty));
                        typeReference = null;
                        this.MoveNext();
                        break;
                    case InvokeMethodExpressionTokenType.Star:
                        if (typeReference != null)
                        {
                            RaiseUnexpectedToken(startToken);
                        }

                        typeReference = ReadWildCard();
                        break;
                    case InvokeMethodExpressionTokenType.Identifier:
                        if (typeReference != null)
                        {
                            RaiseUnexpectedToken(startToken);
                        }

                        typeReference = ReadTypeReference();
                        break;
                    default:
                        RaiseUnexpectedToken(startToken);
                        break;
                }
            }

            // tag on last reference.
            if ((startToken != null && startToken.Type == InvokeMethodExpressionTokenType.Comma) || typeReference != null)
            {
                parameters.Add(typeReference ?? new InvokeMethodTypeReference(string.Empty));
            }

            return parameters;
        }

        private InvokeMethodTypeReference ReadWildCard()
        {
            ReadType(InvokeMethodExpressionTokenType.Star);
            var result = new InvokeMethodTypeReference(string.Empty);
            if (Current.Type == InvokeMethodExpressionTokenType.Star)
            {
                RaiseUnexpectedToken(Current);
            }

            return result;
        }

        private InvokeMethodTypeReference ReadTypeReference()
        {
            var typeName = ReadIdentifier();

            // is nullable?
            if (Current.Type == InvokeMethodExpressionTokenType.QuestionMark)
            {
                MoveNext();
                typeName += "?";
                return new InvokeMethodTypeReference(typeName);
            }

            // is generic?
            if (CurrentType == InvokeMethodExpressionTokenType.LessThan || CurrentType == InvokeMethodExpressionTokenType.OpenCurlyBrace)
            {
                MoveNext();

                // read generic arguments, if none are read then any add default
                var parameters = ReadParameterTypes(InvokeMethodExpressionTokenType.GreaterThan, 
                    InvokeMethodExpressionTokenType.CloseCurlyBrace);
                if (parameters.Count == 0)
                {
                    parameters.Add(new InvokeMethodTypeReference(string.Empty));
                }

                var result = new InvokeMethodTypeReference(typeName, parameters);
                CheckAnyType(InvokeMethodExpressionTokenType.GreaterThan, InvokeMethodExpressionTokenType.CloseCurlyBrace);
                MoveNext();
                return result;
            }

            return new InvokeMethodTypeReference(typeName);
        }

        private string ReadIdentifier()
        {
            CheckType(InvokeMethodExpressionTokenType.Identifier);

            var result = Current.Text;
            if (MoveNext() && Current.Type == InvokeMethodExpressionTokenType.Dot)
            {
                var output = new StringBuilder();
                output.Append(result);
                output.Append(Current.Text);

                var nextType = InvokeMethodExpressionTokenType.Identifier;
                while (this.MoveNext() && Current.Type == nextType)
                {
                    output.Append(Current.Text);
                    nextType = Current.Type == InvokeMethodExpressionTokenType.Identifier
                        ? InvokeMethodExpressionTokenType.Dot
                        : InvokeMethodExpressionTokenType.Identifier;
                }

                if (nextType == InvokeMethodExpressionTokenType.Identifier)
                {
                    throw new FormatException(string.Format("Invalid Identifier \"{0}\"", output));
                }

                result = output.ToString();
            }

            return result;
        }

        private string ReadType(InvokeMethodExpressionTokenType type)
        {
            this.CheckType(type);
            var result = Current.Text;
            this.MoveNext();
            return result;
        }

        private void CheckAnyType(params InvokeMethodExpressionTokenType[] types)
        {
            if (!types.Any(x => x == CurrentType))
            {
                var expectedTypes = string.Join(", ", types.Select(x => x.ToString()).ToArray());
                throw new FormatException(string.Format("Expected one of {0}", expectedTypes));
            }
        }

        private void CheckType(InvokeMethodExpressionTokenType type)
        {
            if (Current.Type != type)
            {
                throw new FormatException(string.Format("Expected {0}", type));
            }
        }
    }
}